package com.legal.service.user.impl;

import com.legal.service.user.SlidingWindowRateLimiter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
 * 实现滑动窗口限流服务
 *
 * @author xiaopeng
 * @version 1.0
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class SlidingWindowRateLimiterImpl implements SlidingWindowRateLimiter {

    private final RedisTemplate<String, Object> redisTemplate;

    private static final String RATE_LIMIT_PREFIX = "rate:limit:sliding:";

    /**
     * 检查是否允许请求,并记录请求记录：
     * 统计当前时间的前一窗口内的请求数，如果超过最大请求数则拒绝请求
     *
     * @param key 请求者的唯一标识，用于在Redis中区分不同的请求者
     * @param windowSizeInSeconds 时间窗口的大小，以秒为单位，在这个时间范围内最多可以发起最大请求数
     * @param maxRequests 在指定时间窗口内允许的最大请求数
     * @return 如果当前请求不违反速率限制，则返回true；否则返回false
     */
    public boolean allowRequest(String key, int windowSizeInSeconds, int maxRequests) {
        // 1. 获取当前时间戳，并计算窗口起始时间戳
        long now = System.currentTimeMillis();
        long windowSizeInMillis = windowSizeInSeconds * 1000L;
        long windowStart = now - windowSizeInMillis;  // 计算窗口起始时间戳

        String redisKey = RATE_LIMIT_PREFIX + key;

        // 2.移除过期的旧请求，使用Redis的ZREMRANGEBYSCORE命令，删除分数在0到窗口起始时间戳的元素
            // ZREMRANGEBYSCORE命令删除指定分数范围内的元素，返回删除的元素数量
            // 有序集合(sorted set/zset)：集合中每个元素关联一个分数(score)，根据分数升序排序，没有重复元素
            // 使用Redis的ZSET数据结构，score的值为时间戳，value为唯一ID
        redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart); // 清除过期的旧请求

        // 3.获取并统计当前窗口内的请求数, 如果为null则表示当前窗口内没有请求
        Long currentCount = redisTemplate.opsForZSet().zCard(redisKey); // zCard()方法返回集合中元素的数量

        // 4.如果当前窗口内的请求数大于等于最大请求数，则拒绝请求
        if (currentCount != null && currentCount >= maxRequests) {
            log.warn("滑动窗口限流触发：{} 当前计数: {}", key, currentCount);
            return false;
        }

        // 5.若当前窗口内的请求数小于最大请求数，则允许请求，并记录请求，添加当前请求到窗口
        String uniqueId = UUID.randomUUID().toString();
        redisTemplate.opsForZSet().add(redisKey, uniqueId, now);

        // 6.设置过期时间，避免长期占用内存
        redisTemplate.expire(redisKey, windowSizeInSeconds + 1, TimeUnit.SECONDS);
        return true;
    }


    /**
     * 只检查但不记录请求（用于预检查）:
     * 检查给定时间窗口内是否可以发起请求
     * 该方法用于实现基于时间窗口的速率限制，以防止超过预定义的请求次数
     *
     * @param key 请求者的唯一标识，用于在Redis中区分不同的请求者
     * @param windowSizeInSeconds 时间窗口的大小，以秒为单位，在这个时间范围内最多可以发起最大请求数
     * @param maxRequests 在指定时间窗口内允许的最大请求数
     * @return 如果当前请求不违反速率限制，则返回true；否则返回false
     */
    public boolean peekRequest(String key, int windowSizeInSeconds, int maxRequests) {
        // 1.获取当前时间戳
        long now = System.currentTimeMillis();
        // 计算时间窗口开始的时间戳
        long windowStart = now - windowSizeInSeconds * 1000L;

        // 2.构造Redis中的键名
        String redisKey = RATE_LIMIT_PREFIX + key;
        // 3.移除时间窗口之外的所有请求记录，确保只保留当前时间窗口内的请求
        redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);

        // 4.获取当前时间窗口内的请求数量
        Long currentCount = redisTemplate.opsForZSet().zCard(redisKey);
        // 5.检查当前请求数量是否达到最大请求限制
        return currentCount == null || currentCount < maxRequests;
    }
}
